文章目錄
  1. 1. 问题描述
  2. 2. 概念详解
  3. 3. 过程分析
  4. 4. 代码说明

建议:阅读本文之前,最好对于TCP的发送、重发以及ACK机制有所了解。

问题描述

最近在一个消息中间件系统(该消息中间件由客户端SDK和服务端Server组成)的性能测试时,发现每个请求的响应时间大概在40ms-50ms之间,这明显过大了。最终定位,是因为SDK没有禁用TCP的Nagle算法导致的。但其根本原理是因为TCP的Delayed Ack机制和Nagle Algorithm相互影响导致的。

概念详解

TCP-Delayed Ack
目的:用于防止只发送一个单独的Ack包,而是希望发送的包是一个Ack+一份数据组成一个包,这样能较少交互次数,减少网络资源消耗。这个设定是基于对于一般交互场景的一个基本假设:数据接收方会生成一个响应给数据发送方。
原理:当数据接收方收到一个TCP包之后,先不回应ACK包,而是等待一定的时间(本例中是40ms),直到

  1. 数据接收方发送一份响应数据
    当数据接收方,收到足够的数据,进行业务处理后,一般会返回业务响应,这时会立即返回:业务响应+ACK。
  2. 接收到连续的TCP包
    当服务接收端接收到1号TCP包后,会先延迟发送1号包的ACK,等待2号包到来,2号包到达时,则立即返回一次ACK;当3号包到来时,会延迟3号包的ACK,等待4号包到来,4号包到达时,则立即返回一次ACK……
    那么,这样就造成偶数序号的TCP包到达的时候,就会立即返回一次ACK;而奇数序号的包到达时,则延迟ACK响应,等待后续的偶数包到来。
  3. 超时
    超过40ms。

TCP-Nagle Algorithm
目的:用于防止发送大量的小包,降低网络资源消耗。
原理:当数据发送方写入TCP缓冲区的数据小于MSS(最大报文长度),则暂不发送,等待写入的数据达到MSS再发送;除非在等待的过程中,发送端发送出去的所有TCP报文均已被ACK,这样就可以不用等待写入数据达到MSS,直接发送出去了。我们称没有达到MSS的报文为小包的话,那么其实TCP-Nagle Algorithm就保证了一个连接在一个时刻,有且只能有一个没有被确认的小包。
关于以上两点,可以参考这篇文章,介绍的很详细:http://www.stuartcheshire.org/papers/NagleDelayedAck/

过程分析

该消息中间件系统在做性能测试时,SDK没有禁用Nagle算法,而Server端禁用了;测试时的消息长度为消息头(12)+消息体(14)Byte,TCP MSS是1460Byte;服务端和SDK交互采用类似TCP三次握手的确认机制来保证高可靠性。那么考虑如下过程:

  1. SDK端发送第一条消息,写入了TCP Buffer,虽然未达到MSS,但是因为没有需要确认的包,所以会立即发送;
  2. Server端收到TCP包后,就延迟ACK响应;同时SDK端由于启用了Nagle算法,并且存在没有ACK的包,因此处于等待中;
  3. Server端业务层解包进行业务处理,处理完成后,立即发送业务响应,并捎带ACK返回给SDK;
  4. SDK端收到了Server端返回的响应和ACK后,立即回复业务确认消息,并捎带ACK;
  5. 由于Server端收到业务确认消息后,不用再返回响应给SDK,因此延迟ACK确认;
  6. SDK端发送第二条消息,但是因为启用了Nagle算法,所以必须等待ACK。直到第5步的延迟确认到达,则立即发送第二条消息。

Tcpdump抓包如下:

1
2015-05-25 18:42:49.736897 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 71:101(30) ack 20 win 16420
E..FU3@.=..OdT4ZdTI-.}#...(.R.mpP.@$.).......)....
queueAThis is test 
2015-05-25 18:42:49.737763 IP 100.84.73.45.9090 > 100.84.52.90.58237: P 20:39(19) ack 101 win 46
E..;Fn@.@...dTI-dT4Z#..}R.mp..)	P...F].......*......SUCCESS
2015-05-25 18:42:49.744738 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 101:113(12) ack 39 win 16415
E..4U4@.=..`dT4ZdTI-.}#...)	R.m.P.@.d........4......
2015-05-25 18:42:49.784810 IP 100.84.73.45.9090 > 100.84.52.90.58237: . ack 113 win 46
E..(Fo@.@..1dTI-dT4Z#..}R.m...).P.......
2015-05-25 18:42:49.785438 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 113:143(30) ack 39 win 16415
E..FU5@.=..MdT4ZdTI-.}#...).R.m.P.@..........)....
queueAThis is test 
2015-05-25 18:42:49.785450 IP 100.84.73.45.9090 > 100.84.52.90.58237: . ack 143 win 46
E..(Fp@.@..0dTI-dT4Z#..}R.m...)3P.......
2015-05-25 18:42:49.786147 IP 100.84.73.45.9090 > 100.84.52.90.58237: P 39:58(19) ack 143 win 46
E..;Fq@.@...dTI-dT4Z#..}R.m...)3P...F].......*......SUCCESS
2015-05-25 18:42:49.790470 IP 100.84.52.90.58237 > 100.84.73.45.9090: P 143:173(30) ack 58 win 16410
E..(Fo@.@..1dTI-dT4Z#..}R.m...).P.......

总结:通过以上过程可以发现,由于第5步的延迟ACK,导致SDK发送消息延迟,最终导致了性能下降。如果SDK端关闭的Nagle算法,SDK就可以立即发送第二条消息,而不会受到Nagle算法的约束,一直等待第5步的ACK到达。

代码说明

在Netty中,可以通过设置tcpNoDelay选项,开启或者禁用Nagle算法。

1
2
3
4
5
6
7
8
// 用来禁用TCP的Nagle算法。
bootstrap.setOption("tcpNoDelay", true);

// Netty底层其实就是使用JDK中的Socket.class的setTcpNoDelay方法来设置。
/**
* Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm).
*/

public void setTcpNoDelay(boolean on) throws SocketException;

文章目錄
  1. 1. 问题描述
  2. 2. 概念详解
  3. 3. 过程分析
  4. 4. 代码说明